package com.fable.sp.core.service;

import com.fable.sp.core.domain.*;
import com.fable.sp.core.enums.ColumnType;
import com.fable.sp.core.enums.ContentType;
import com.fable.sp.core.repository.*;
import com.fable.sp.core.service.impl.ContainerContextImpl;
import com.fable.sp.core.service.impl.RenderRequestImpl;
import com.fable.sp.core.service.impl.RenderResponseImpl;
import com.fable.sp.core.service.impl.TableBuilder;
import com.fable.sp.core.template.TemplateNotFoundException;
import com.fable.sp.core.template.TemplateRenderException;
import com.fable.sp.core.template.TemplateRenderer;
import com.fable.sp.core.util.CollectionUtils;
import com.fable.sp.core.util.IOUtils;
import com.fable.sp.core.widget.*;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.slyak.spring.jpa.GenericJpaRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.support.Repositories;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

/**
 * .
 *
 * @author stormning on 16/8/1.
 */
@Service
@Transactional(value = "spTxManager", readOnly = true)
public class WidgetService implements ApplicationContextAware {

    private static final Logger LOGGER = LoggerFactory.getLogger(WidgetService.class);
    private static final String[] COPY_IGNORED_PROPS = new String[]{"id", "createdBy", "createdDate", "lastModifiedBy", "lastModifiedDate", "version", "status"};
    @Autowired
    private LayoutRepository layoutRepository;
    @Autowired
    private LayoutItemRepository layoutItemRepository;
    @Autowired
    private ContainerRepository containerRepository;
    @Autowired
    private WidgetInstanceRepository widgetInstanceRepository;
    @Autowired
    private TemplateRenderer templateRenderer;
    @Autowired
    private WidgetRenderer widgetRenderer;
    @Autowired
    private LayoutItemContentRepository layoutItemContentRepository;
    @Autowired
    private ContainerTableRepository containerTableRepository;
    @Autowired
    private TableBuilder tableBuilder;
    private String layoutTemplateName = "/global/layout.ftl";
    private Map<String, ConfigurableWidget> widgetNameAndConfigs = Maps.newHashMap();
    private Map<String, List<ConfigurableWidget>> widgetTagAndConfigs = Maps.newHashMap();
    private ApplicationContext applicationContext;
    private ConversionService conversionService = new DefaultConversionService();
    private Repositories repositories;

    public Layout getOriginalLayout(Long containerId) {
        return layoutRepository.findByContainerIdAndOriginal(containerId, true);
    }

    public Container getContainer(Long containerId) {
        return containerRepository.findOne(containerId);
    }

    @Transactional("spTxManager")
    public Container saveContainer(Container container) {
        Long id = container.getId();
        if (id == null) {
            Container saved = containerRepository.save(container);
            Long pid = container.getPid();
            if (pid == null) {
                saved.setPath(String.valueOf(saved.getId()) + "|");
            } else {
                Container pc = getContainer(pid);
                saved.setPath(pc.getPath() + String.valueOf(saved.getId()) + "|");
            }
            return containerRepository.save(container);
        } else {
            Container exist = containerRepository.findOne(id);
            exist.setName(container.getName());
            exist.setInfo(container.getInfo());
            return containerRepository.save(exist);
        }
    }

    public Page<Container> queryContainers(ContainerQuery query, Pageable pageable) {
        return containerRepository.queryContainers(query, pageable);
    }

    @Transactional("spTxManager")
    public Layout saveLayout(Layout layout) {
        return layoutRepository.save(layout);
    }

    @Transactional("spTxManager")
    public WidgetInstance saveWidgetInstance(WidgetInstance widgetInstance) {
        return widgetInstanceRepository.save(widgetInstance);
    }

    @Transactional("spTxManager")
    public LayoutItem saveLayoutItem(LayoutItem layoutItem) {
        Long id = layoutItem.getId();
        if (id == null) {
            return layoutItemRepository.save(layoutItem);
        } else {
            LayoutItem exist = getLayoutItem(id);
            exist.setConfig(layoutItem.getConfig());
            return layoutItemRepository.save(exist);
        }
    }

    @Transactional("spTxManager")
    public void removeLayoutItem(Long layoutItemId) {
        List<LayoutItemContent> contents = layoutItemContentRepository.findByLayoutItemId(layoutItemId);
        //if layout item has content ,set content's layoutItemId to 0
        if (contents.size() > 0) {
            for (LayoutItemContent content : contents) {
                content.setLayoutItemId(0);
                saveLayoutItemContent(content);
            }
        }
        layoutItemRepository.delete(layoutItemId);
    }


    @Transactional("spTxManager")
    public Layout createOriginalLayout(Long containerId) {
        Container container = containerRepository.findOne(containerId);
        if (container != null) {
            Layout layout = new Layout();
            layout.setOriginal(true);
            layout.setContainerId(containerId);
            return layoutRepository.save(layout);
        }
        return null;
    }

    public LayoutItem getLayoutItem(Long id) {
        return layoutItemRepository.findOne(id);
    }

    public WidgetInstance getWidgetInstance(Long id) {
        return widgetInstanceRepository.findOne(id);
    }

    public Layout getLayout(Long id) {
        return layoutRepository.findOne(id);
    }

    public List<LayoutItem> getLayoutItems(Long layoutId) {
        List<LayoutItem> items = layoutItemRepository.findByLayoutId(layoutId);
        for (LayoutItem item : items) {
            //avoid dirty record caused by cache
            List<LayoutItemContent> contents = item.getContents();
            if (contents != null) {
                contents.clear();
            }
        }
        sort(items);
        return items;
    }

    private void sort(List<LayoutItem> layoutItems) {
        Collections.sort(layoutItems, new Comparator<LayoutItem>() {
            @Override
            public int compare(LayoutItem o1, LayoutItem o2) {
                int r1 = o1.getConfig().getY() - o2.getConfig().getY();
                if (r1 == 0) {
                    return o1.getConfig().getX() - o2.getConfig().getX();
                }
                return r1;
            }
        });
    }

    @Transactional("spTxManager")
    public List<LayoutItem> saveLayoutItems(List<LayoutItem> layoutItems) {
        List<LayoutItem> items = Lists.newArrayList();
        //save layoutItems
        for (LayoutItem layoutItem : layoutItems) {
            items.add(saveLayoutItem(layoutItem));
        }
        return items;
    }

    @Transactional("spTxManager")
    public LayoutItemContent addWidget(String widgetName, Long itemId) {
        if (StringUtils.hasText(widgetName) && itemId != null) {
            LayoutItem layoutItem = getLayoutItem(itemId);
            WidgetInstance instance = new WidgetInstance();
            instance.setWidgetName(widgetName);
            instance.setAttributes(getDefaultAttributes(widgetName));
            WidgetInstance wp = saveWidgetInstance(instance);

            LayoutItemContent content = new LayoutItemContent();
            content.setContentType(ContentType.WIDGET);
            content.setContentId(wp.getId());
            content.setLayoutItemId(itemId);
            content.setLayoutId(layoutItem.getLayoutId());
            saveLayoutItemContent(content);

            Widget widget = getWidget(widgetName);
            if (widget instanceof WidgetLifeCycle) {
                Layout layout = getLayout(layoutItem.getLayoutId());
                ((WidgetLifeCycle) widget).afterInstanceCreated(instance, layout.getContainerId());
            }
            return content;
        }
        return null;
    }

    @Transactional("spTxManager")
    public LayoutItemContent saveLayoutItemContent(LayoutItemContent content) {
        LayoutItemContent exist = layoutItemContentRepository.findByLayoutIdAndContentId(content.getLayoutId(), content.getContentId());
        if (exist == null) {
            return layoutItemContentRepository.save(content);
        } else {
            exist.setLayoutItemId(content.getLayoutItemId());
            exist.setLayoutId(content.getLayoutId());
            exist.setContentId(content.getContentId());
            return layoutItemContentRepository.save(exist);
        }
    }

    private Map<String, String> getDefaultAttributes(String widgetName) {
        Widget widget = getWidget(widgetName);
        Map<String, String> attributes = Maps.newHashMap();
        if (widget instanceof ConfigurableWidget) {
            List<Preference> preferences = ((ConfigurableWidget) widget).getPreferences();
            if (!CollectionUtils.isEmpty(preferences)) {
                for (Preference preference : preferences) {
                    attributes.put(preference.getName(), preference.getValue());
                }
            }
        }
        return attributes;
    }

    public Map<String, ConfigurableWidget> getWidgetConfigs() {
        return Collections.unmodifiableMap(widgetNameAndConfigs);
    }

    public List<ConfigurableWidget> queryWidgets(WidgetQuery query) {
        return queryWidgets(widgetNameAndConfigs.values(), query);
    }

    private List<ConfigurableWidget> queryWidgets(Collection<ConfigurableWidget> toSearch, WidgetQuery query) {
        if (org.springframework.util.CollectionUtils.isEmpty(toSearch)) {
            return Collections.emptyList();
        }
        if (query == null) {
            return Lists.<ConfigurableWidget>newArrayList(toSearch);
        }
        return Lists.newArrayList(filter(toSearch, query));
    }

    private List<ConfigurableWidget> filter(Collection<ConfigurableWidget> toSearch, WidgetQuery query) {
        List<ConfigurableWidget> configs = Lists.newArrayList();
        for (ConfigurableWidget cfg : toSearch) {
            String name = query.getName();
            if (StringUtils.hasText(name)) {
                if (StringUtils.countOccurrencesOf(cfg.getInfo(), name) == 0 && StringUtils.countOccurrencesOf(cfg.getTitle(), name) == 0) {
                    continue;
                }
            }
            String tag = query.getTag();
            if (StringUtils.hasText(tag)) {
                String tags = cfg.getTags();
                String[] splits = org.apache.commons.lang3.StringUtils.split(tags, " ");
                if (splits == null || splits.length == 0 || Arrays.binarySearch(splits, tag) < 0) {
                    continue;
                }
            }
            configs.add(cfg);
        }
        return configs;
    }

    public Map<String, Integer> getWidgetTagMap(WidgetQuery widgetQuery) {
        Map<String, Integer> tagMap = Maps.newHashMap();
        for (String key : widgetTagAndConfigs.keySet()) {
            List<ConfigurableWidget> configs = widgetTagAndConfigs.get(key);
            tagMap.put(key, queryWidgets(configs, widgetQuery).size());
        }
        return tagMap;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        widgetNameAndConfigs.clear();
        widgetTagAndConfigs.clear();

        this.applicationContext = applicationContext;
        this.repositories = new Repositories(applicationContext);
        Map<String, ConfigurableWidget> widgetConfigMap = applicationContext.getBeansOfType(ConfigurableWidget.class);
        if (!org.springframework.util.CollectionUtils.isEmpty(widgetConfigMap)) {
            for (ConfigurableWidget config : widgetConfigMap.values()) {
                widgetNameAndConfigs.put(config.getClass().getName(), config);
                String tags = config.getTags();
                String[] splits = org.apache.commons.lang3.StringUtils.split(tags, " ");
                if (splits != null && splits.length > 0) {
                    for (String tag : splits) {
                        List<ConfigurableWidget> tagCfgs = widgetTagAndConfigs.get(tag);
                        if (tagCfgs == null) {
                            tagCfgs = Lists.newArrayList();
                            widgetTagAndConfigs.put(tag, tagCfgs);
                        }
                        tagCfgs.add(config);
                    }
                }
            }
        }
    }

    public String renderLayout(Long layoutId, HttpServletRequest request, HttpServletResponse response) throws WidgetException {
        Layout layout = getLayout(layoutId);
        Container container = getContainer(layout.getContainerId());
        return render(container, layout, request, response);
    }

    public String renderLayout(Long layoutId, Long recordId, HttpServletRequest request, HttpServletResponse response) throws WidgetException {
        return renderLayout(getLayout(layoutId), recordId, request, response);
    }

    public String renderLayout(Layout layout, Long recordId, HttpServletRequest request, HttpServletResponse response) throws WidgetException {
        long containerId = layout.getContainerId();
        Container container = getContainer(containerId);
        initContainerColumnValues(recordId, container, containerId);
        return render(container, layout, request, response);
    }

    public List<Layout> renderLayouts(LinkedHashMap<Long, String> layoutModes, Long recordId, HttpServletRequest request, HttpServletResponse response) throws WidgetException {
        List<Layout> renderedLayout = Lists.newArrayList();
        Container container = null;
        for (Map.Entry<Long, String> lme : layoutModes.entrySet()) {
            Long layoutId = lme.getKey();
            Layout layout = getLayout(layoutId);
            layout.setMode(lme.getValue());
            if (container == null) {
                long containerId = layout.getContainerId();
                container = getContainer(containerId);
                initContainerColumnValues(recordId, container, containerId);
            }
            Layout copy = new Layout();
            BeanUtils.copyProperties(layout, copy);
            copy.setRendered(render(container, layout, request, response));
            renderedLayout.add(copy);
        }
        return renderedLayout;
    }

    private void initContainerColumnValues(Long recordId, Container container, long containerId) {
        if (container != null && recordId != null && recordId > 0) {
            Object record = tableBuilder.getRecord(containerId, recordId);
            if (record != null) {
                ContainerTable containerTable = containerTableRepository.findOne(containerId);
                Map<Long, ColumnDef> instanceColumns = containerTable.getInstanceColumns();
                Map<Long, Object> instanceColumnValues = Maps.newHashMap();
                Map<String, Object> columnValues = Maps.newHashMap();
                for (Map.Entry<Long, ColumnDef> defEntry : instanceColumns.entrySet()) {
                    ColumnDef def = defEntry.getValue();
                    String name = def.getRealColumnName();
                    Object columnValue = tableBuilder.getColumnValue(containerId, record, name);
                    instanceColumnValues.put(defEntry.getKey(), columnValue);
                    columnValues.put(def.getName(), columnValue);
                }
                container.setColumnValues(columnValues);
                container.setInstanceColumnValues(instanceColumnValues);
            }
        }
    }


    public String renderContainer(Long containerId, HttpServletRequest request, HttpServletResponse response) throws WidgetException {
        Container container = getContainer(containerId);
        Layout layout = getOriginalLayout(containerId);
        return render(container, layout, request, response);
    }

    private int getLayoutHeight(List<LayoutItem> items, int cellHeight) {
        if (items == null) {
            return 0;
        }
        int maxHeight = 0;
        for (LayoutItem item : items) {
            LayoutItemConfig cfg = item.getConfig();
            maxHeight = Math.max(cfg.getHeight() + cfg.getY(), maxHeight);
        }
        return cellHeight * maxHeight;
    }

    private String render(Container container, Layout layout, HttpServletRequest request, HttpServletResponse response) throws WidgetException {
        Map<String, Object> params = Maps.newHashMap();
        params.put("container", container);
        if (container != null) {
            params.put("containerId", layout.getContainerId());
            container.setCurrentLayout(layout);
        }
        params.put("layout", layout);
        params.put("layoutId", layout.getId());
        List<LayoutItem> layoutItems = getLayoutItems(layout.getId());
        params.put("layoutItems", layoutItems);
        layout.setHeight(getLayoutHeight(layoutItems, layout.getCellHeight()));

        //set global attributes
        for (Map.Entry<String, Object> pe : params.entrySet()) {
            request.setAttribute(pe.getKey(), pe.getValue());
        }

        Map<Long, List<LayoutItemContent>> itemContents = Maps.newHashMap();
        for (LayoutItemContent content : getLayoutItemContents(layout.getId())) {
            if (content.getLayoutItemId() == 0) {
                continue;
            }
            long contentId = content.getContentId();
            if (content.getContentType() == ContentType.LAYOUT) {
                //TODO need remove, only content type support widget only.
                content.setContent(renderLayout(contentId, request, response));
            } else {
                //default mode is edit
                String mode = layout.getMode();
                if (!StringUtils.hasText(mode)) {
                    mode = org.apache.commons.lang3.StringUtils.defaultString(content.getMode(), Widget.RENDER_MODE_EDIT);
                }
                content.setContent(renderWidget(contentId, container, mode, request, response));
            }
            long layoutItemId = content.getLayoutItemId();
            List<LayoutItemContent> lics = itemContents.get(layoutItemId);
            if (lics == null) {
                lics = Lists.newArrayList();
                itemContents.put(layoutItemId, lics);
            }
            lics.add(content);
        }
        for (LayoutItem item : layoutItems) {
            List<LayoutItemContent> contents = itemContents.get(item.getId());
            if (!org.springframework.util.CollectionUtils.isEmpty(contents)) {
                Collections.sort(contents);
                item.setContents(contents);
            }
        }
        return templateRenderer.renderNamedTemplate(layoutTemplateName, params);
    }

    public String renderWidget(long instanceId, HttpServletRequest request, HttpServletResponse response) {
        String mode = request.getParameter(RenderRequest.RENDER_MODE_NAME);
        return renderWidget(instanceId, null, mode, request, response);
    }

    private String renderWidget(long instanceId, Container container, String mode, HttpServletRequest request, HttpServletResponse response) {
        WidgetInstance instance = widgetInstanceRepository.findOne(instanceId);
        return renderWidget(instance, container, mode, request, response);
    }

    private String renderWidget(WidgetInstance instance, Container container, String mode, HttpServletRequest request, HttpServletResponse response) {
        try {
            RenderRequestImpl renderRequest = new RenderRequestImpl(instance, request, applicationContext);
            if (StringUtils.hasText(mode)) {
                renderRequest.setAttribute(RenderRequest.RENDER_MODE_NAME, mode);
            }
            renderRequest.setContainerContext(new ContainerContextImpl(container));
            RenderResponse renderResponse = new RenderResponseImpl();
            return renderWidget(renderRequest, renderResponse);
        } catch (Exception e) {
            LOGGER.error("Render widget error.", e);
            return null;
        }
    }

    private String renderWidget(RenderRequest renderRequest, RenderResponse renderResponse) {
        try {
            Widget widget = getWidget(renderRequest.getWidgetInstance().getWidgetName());
            if (!(widget instanceof Persistentable) && WidgetMode.EDIT.equals(renderRequest.getWidgetMode())) {
                renderRequest.setAttribute(RenderRequest.RENDER_MODE_NAME, Widget.RENDER_MODE_VIEW);
            }
            //before
            boolean lifeCycleWidget = widget instanceof WidgetLifeCycle;
            if (lifeCycleWidget) {
                ((WidgetLifeCycle) widget).beforeRender(renderRequest, renderResponse);
            }
            widget.render(renderRequest, renderResponse);
            String result = widgetRenderer.render(renderRequest, renderResponse);
            //after
            if (lifeCycleWidget) {
                ((WidgetLifeCycle) widget).afterRender(renderRequest, renderResponse);
            }
            return result;

        } catch (Exception e) {
            LOGGER.error("Render widget error.", e);
            return null;
        }
    }

    public List<LayoutItemContent> getLayoutItemContents(Long layoutId) {
        return layoutItemContentRepository.findByLayoutId(layoutId);
    }

    @Transactional("spTxManager")
    public void updateLayoutItemContents(List<LayoutItemContent> itemContents) {
        for (LayoutItemContent changed : itemContents) {
            LayoutItemContent content = getLayoutItemContent(changed.getId());
            content.setLayoutItemId(changed.getLayoutItemId());
            content.setWeight(changed.getWeight());
            saveLayoutItemContent(content);
        }
    }

    public List<PersistentField> getLayoutFields(Long layoutId, boolean simple) {
        List<LayoutItemContent> layoutItemContents = getLayoutItemContents(layoutId);
        List<PersistentField> fields = Lists.newArrayList();
        for (LayoutItemContent content : layoutItemContents) {
            if (content.getContentType() == ContentType.WIDGET && content.getLayoutItemId() > 0) {
                Long instanceId = content.getContentId();
                WidgetInstance instance = getWidgetInstance(instanceId);
                if (instance != null) {
                    Widget widget = getWidget(instance.getWidgetName());
                    if(!simple && !(widget instanceof Persistentable)){
                        continue;
                    }
                    //if (widget instanceof Persistentable) {
                        PersistentField field = new PersistentField();
                        field.setName((simple?"":PersistentField.FIELD_NAME_PREFIX) + instanceId);
                        field.setDisplay(instance.getDisplayName());
                        fields.add(field);
                    //}
                }
            }
        }
        return fields;
    }

    public List<WidgetInstance> getContainerWidgetInstances(Long containerId) {
        Layout originalLayout = getOriginalLayout(containerId);
        List<LayoutItemContent> layoutItemContents = getLayoutItemContents(originalLayout.getId());
        List<WidgetInstance> instances = Lists.newArrayList();
        for (LayoutItemContent content : layoutItemContents) {
            if (content.getContentType() == ContentType.WIDGET) {
                instances.add(getWidgetInstance(content.getContentId()));
            }
        }
        return instances;
    }

    public Widget getWidgetByInstanceId(Long instanceId) {
        WidgetInstance instance = getWidgetInstance(instanceId);
        if (instance != null) {
            return getWidget(instance.getWidgetName());
        }
        return null;
    }

    public Widget getWidget(String widgetName) {
        try {
            return (Widget) applicationContext.getBean(ClassUtils.forName(widgetName, ClassUtils.getDefaultClassLoader()));
        } catch (ClassNotFoundException e) {
            LOGGER.error("Widget not found.", e);
            return null;
        }
    }

    public ContainerTable getContainerTable(Long containerId) {
        return containerTableRepository.findOne(containerId);
    }

    @Transactional("spTxManager")
    public boolean saveContainerTable(Long containerId, Map<Long, ColumnDef> instanceColumns) {
        try {
            ContainerTable containerTable = tableBuilder.saveContainerTable(containerId, instanceColumns);
            containerTableRepository.save(containerTable);
            return true;
        } catch (Exception e) {
            LOGGER.error("Save container table error", e);
            return false;
        }
    }

    public List<Layout> listLayouts(LayoutQuery layoutQuery) {
        return layoutRepository.listLayouts(layoutQuery);
    }

    @Transactional("spTxManager")
    public Layout saveLayoutControl(Layout layout) {
        if (layout.getId() != null && layout.getId() > 0) {
            Assert.hasText(layout.getName(), "Layout name must be set");
        }
        Layout result;
        if (layout.getId() == null) {
            Layout sourceLayout;
            long containerId = layout.getContainerId();
            if (layout.getCopyId() == null) {
                sourceLayout = getOriginalLayout(containerId);
            } else {
                sourceLayout = getLayout(layout.getCopyId());
            }
            Long sourceId = sourceLayout.getId();
            //copy layout
            result = new Layout();
            BeanUtils.copyProperties(sourceLayout, result, COPY_IGNORED_PROPS);
            result.setOriginal(false);
            result.setName(layout.getName());
            result.setInfo(layout.getInfo());
            saveLayout(result);

            //copy layout item
            List<LayoutItem> layoutItems = getLayoutItems(sourceId);
            if (!org.springframework.util.CollectionUtils.isEmpty(layoutItems)) {
                //copy layout item content
                List<LayoutItemContent> contents = getLayoutItemContents(sourceId);

                Map<Long, List<LayoutItemContent>> contentMap = CollectionUtils.toMapList(contents, new Function<LayoutItemContent, Long>() {
                    @Override
                    public Long apply(LayoutItemContent layoutItemContent) {
                        return layoutItemContent.getLayoutItemId();
                    }
                });

                for (LayoutItem layoutItem : layoutItems) {
                    //copy item
                    LayoutItem itemCopy = new LayoutItem();
                    BeanUtils.copyProperties(layoutItem, itemCopy, COPY_IGNORED_PROPS);
                    itemCopy.setLayoutId(result.getId());
                    saveLayoutItem(itemCopy);

                    List<LayoutItemContent> cs = contentMap.get(layoutItem.getId());
                    if (!CollectionUtils.isEmpty(cs)) {
                        for (LayoutItemContent content : cs) {
                            //copy item content
                            LayoutItemContent contentCopy = new LayoutItemContent();
                            BeanUtils.copyProperties(content, contentCopy, COPY_IGNORED_PROPS);
                            contentCopy.setLayoutId(result.getId());
                            contentCopy.setLayoutItemId(itemCopy.getId());
                            saveLayoutItemContent(contentCopy);
                        }
                    }
                }
            }
        } else {
            //save layout info
            result = getLayout(layout.getId());
            result.setName(layout.getName());
            result.setInfo(layout.getInfo());
            saveLayout(result);
        }

        return result;
    }

    public List<LayoutItemContent> getUnusedContents(Long layoutId) {
        Layout current = getLayout(layoutId);
        long containerId = current.getContainerId();
        Layout original = getOriginalLayout(containerId);

        List<LayoutItemContent> originalContents = getLayoutItemContents(original.getId());
        if (CollectionUtils.isEmpty(originalContents)) {
            return Collections.emptyList();
        }

        List<LayoutItemContent> currentContents = getLayoutItemContents(current.getId());

        List<LayoutItemContent> unused = Lists.newArrayList();
        for (LayoutItemContent oc : originalContents) {
            if (!currentContents.contains(oc)) {
                unused.add(oc);
            }
        }

        for (LayoutItemContent cc : currentContents) {
            if (cc.getLayoutItemId() == 0) {
                unused.add(cc);
            }
        }

        return wrapContent(unused);
    }

    private List<LayoutItemContent> wrapContent(List<LayoutItemContent> unused) {
        for (LayoutItemContent content : unused) {
            if (content.getContentType() == ContentType.WIDGET) {
                content.setInstance(getWidgetInstance(content.getContentId()));
            } else {
                content.setInstance(getLayout(content.getLayoutId()));
            }
        }
        return unused;
    }

    public LayoutItemContent getLayoutItemContent(Long id) {
        return layoutItemContentRepository.findOne(id);
    }

    @Transactional("spTxManager")
    public void removeLayoutItemContent(Long id) {
        LayoutItemContent content = getLayoutItemContent(id);
        content.setLayoutItemId(0);
        saveLayoutItemContent(content);
    }

    @Transactional("spTxManager")
    public LayoutItemContent saveLayoutItemContentStyle(LayoutItemContent content) {
        LayoutItemContent exist = getLayoutItemContent(content.getId());
        exist.setStyle(content.getStyle());
        return layoutItemContentRepository.save(exist);
    }

    @Transactional("spTxManager")
    public Layout saveGlobalLayoutItemStyle(Layout layout) {
        Layout exist = getLayout(layout.getId());
        exist.setCellBorder(layout.getCellBorder());
        exist.setCellHeight(layout.getCellHeight());
        return saveLayout(exist);
    }

    public String renderWidgetPreference(Long widgetId, HttpServletRequest request) {
        return renderWidget(widgetId, null, Widget.RENDER_MODE_PREFERENCE, request, null);
    }

    @Transactional("spTxManager")
    public void saveWidgetPreferences(Long widgetId, List<Preference> preferences) {
        if (CollectionUtils.isEmpty(preferences)) {
            return;
        }

        WidgetInstance instance = getWidgetInstance(widgetId);
        Map<String, String> attributes = instance.getAttributes();
        if (attributes == null) {
            attributes = Maps.newHashMap();
        } else {
            attributes = Maps.newHashMap(attributes);
        }
        String widgetName = instance.getWidgetName();
        Widget widget = getWidget(widgetName);
        if (widget instanceof ConfigurableWidget) {
            ConfigurableWidget cfg = (ConfigurableWidget) widget;
            List<Preference> prefers = cfg.getPreferences();

            for (Preference preference : preferences) {
                //check preference?
                if (prefers != null && prefers.contains(preference)) {
                    attributes.put(preference.getName(), preference.getValue());
                }
            }
            instance.setAttributes(attributes);
            cfg.beforeSavePreference(instance);
            WidgetInstance saved = saveWidgetInstance(instance);
            cfg.afterSavePreference(saved);
        }
    }

    @Transactional("spTxManager")
    public Layout saveLayoutStyle(Layout layout) {
        Layout exist = getLayout(layout.getId());
        exist.setStyle(layout.getStyle());
        return saveLayout(exist);
    }


    public void renderAndWrap(String innerTemplate, String wrapper, Map<String, Object> renderArgs, HttpServletResponse response) throws TemplateRenderException, TemplateNotFoundException, IOException {
        wrapper = wrapper == null ? "default" : wrapper;
        String rendered = templateRenderer.renderNamedTemplate("/sp/" + innerTemplate + ".ftl", renderArgs);
        renderArgs.put("rendered", rendered);
        String result;
        try {
            result = templateRenderer.renderNamedTemplate("/global/wrapper/" + innerTemplate + ".ftl", renderArgs);
        } catch (TemplateNotFoundException tnfe) {
            result = templateRenderer.renderNamedTemplate("/global/wrapper/" + wrapper + ".ftl", renderArgs);
        }
        response.setContentType("text/html; charset=UTF-8");
        IOUtils.write(result, response.getOutputStream(), "UTF-8");
    }

    public void renderAndWrap(String innerTemplate, Map<String, Object> renderArgs, HttpServletResponse response) throws TemplateRenderException, TemplateNotFoundException, IOException {
        renderAndWrap(innerTemplate, null, renderArgs, response);
    }


    @Transactional("spTxManager")
    public Long saveRecord(Long layoutId, Long recordId, Map<Long, String> fieldValues) {
        if (fieldValues == null) {
            return recordId;
        }
        Layout layout = getLayout(layoutId);
        long containerId = layout.getContainerId();
        ContainerTable containerTable = getContainerTable(containerId);
        if (containerTable != null) {
            Map<Long, ColumnDef> instanceColumns = containerTable.getInstanceColumns();
            if (!CollectionUtils.isEmpty(instanceColumns)) {
                Object rec;
                if (recordId == null || recordId == 0) {
                    rec = tableBuilder.newRecord(containerId);
                } else {
                    rec = tableBuilder.getRecord(containerId, recordId);
                }
                List<LayoutItemContent> contents = getLayoutItemContents(layoutId);
                if (!CollectionUtils.isEmpty(contents)) {
                    for (LayoutItemContent content : contents) {
                        String mode = content.getMode();
                        long instanceId = content.getContentId();
                        if (WidgetMode.EDIT.toString().equals(mode) && content.getContentType() == ContentType.WIDGET && content.getLayoutItemId() > 0) {
                            ColumnDef columnDef = instanceColumns.get(instanceId);
                            if (columnDef != null) {
                                String value = fieldValues.get(instanceId);
                                String name = columnDef.getRealColumnName();
                                Object realValue = null;
                                if (value != null) {
                                    realValue = conversionService.convert(value, determineType(columnDef));
                                }
                                com.fable.sp.core.util.BeanUtils.setProperty(rec, name, realValue);
                            }
                        }
                    }
                }
                return tableBuilder.saveOrUpdateRecord(rec);
            }
        }
        return recordId;
    }

    private Class determineType(ColumnDef columnDef) {
        ColumnType columnType = columnDef.getColumnType();
        int length = columnDef.getLength();
        if (columnType == ColumnType.BOOLEAN) {
            return Boolean.class;
        } else if (columnType == ColumnType.NUMBER) {
            return length >= 8 ? Long.class : Integer.class;
        } else {
            return String.class;
        }
    }

    @Transactional("spTxManager")
    public LayoutItemContent saveContentMode(Long contentId, String mode) {
        LayoutItemContent content = getLayoutItemContent(contentId);
        content.setMode(mode);
        return layoutItemContentRepository.save(content);
    }

    @Transactional("spTxManager")
    public void fakeDelete(String className, Long id) {
        try {
            GenericJpaRepository repository = (GenericJpaRepository) repositories.getRepositoryFor(ClassUtils.forName(className, ClassUtils.getDefaultClassLoader()));
            repository.fakeDelete(id);
        } catch (ClassNotFoundException e) {
            LOGGER.error("Class not found", e);
        }
    }

    @Transactional("spTxManager")
    public Layout saveLayoutControlInfo(Layout layout) {
        if (layout.getId() != null) {
            Layout exist = getLayout(layout.getId());
            if (exist != null) {
                exist.setName(layout.getName());
                exist.setInfo(layout.getInfo());
                return saveLayout(exist);
            }
        }
        return null;
    }

    @Transactional("spTxManager")
    public WidgetInstance saveWidgetInstanceInfo(Long id, String info) {
        WidgetInstance instance = getWidgetInstance(id);
        if (instance != null) {
            instance.setInfo(info);
            return saveWidgetInstance(instance);
        }
        return null;
    }

    public String renderLayoutItem(Long layoutItemId, HttpServletRequest request, HttpServletResponse response) {
        List<LayoutItemContent> contents = layoutItemContentRepository.findByLayoutItemId(layoutItemId);
        StringBuilder builder = new StringBuilder();
        if (!org.springframework.util.CollectionUtils.isEmpty(contents)) {
            Collections.sort(contents);
            for (LayoutItemContent content : contents) {
                builder.append(renderLayoutItemContent(content, request, response));
            }
        }
        return builder.toString();
    }

    private String wrapContent(LayoutItemContent content) {
        Map<String, Object> params = Maps.newHashMap();
        params.put("content", content);
        try {
            return templateRenderer.renderNamedTemplate("/sp/itemContent.ftl", params);
        } catch (Exception e) {
            LOGGER.error("wrap widget error", e);
            return org.apache.commons.lang3.StringUtils.EMPTY;
        }
    }

    @Transactional("spTxManager")
    public LayoutItem saveLayoutItemStyle(LayoutItem layoutItem) {
        LayoutItem exist = getLayoutItem(layoutItem.getId());
        exist.setStyle(layoutItem.getStyle());
        return layoutItemRepository.save(exist);
    }

    public String renderLayoutItemContent(Long id, HttpServletRequest request, HttpServletResponse response) {
        LayoutItemContent content = getLayoutItemContent(id);
        return renderLayoutItemContent(content, request, response);
    }

    private String renderLayoutItemContent(LayoutItemContent content, HttpServletRequest request, HttpServletResponse response) {
        if (content.getContentType() == ContentType.WIDGET) {
            Layout layout = getLayout(content.getLayoutId());
            Container container = getContainer(layout.getContainerId());
            container.setCurrentLayout(layout);
            String str = renderWidget(content.getContentId(), container, content.getMode(), request, response);
            content.setContent(str);
            return wrapContent(content);
        }
        return org.apache.commons.lang3.StringUtils.EMPTY;
    }

    public Map<Long, Object> getInstanceColumnValues(Long layoutId, Long recordId, Class<? extends Widget> widgetClass) {
        Layout layout = getLayout(layoutId);
        long containerId = layout.getContainerId();
        Object record = tableBuilder.getRecord(containerId, recordId);
        if (record == null) {
            return Collections.emptyMap();
        }
        ContainerTable containerTable = containerTableRepository.findOne(containerId);
        Map<Long, ColumnDef> instanceColumns = containerTable.getInstanceColumns();
        Map<Long, Object> instanceColumnValues = Maps.newHashMap();
        boolean filterClass = widgetClass != null;
        for (Map.Entry<Long, ColumnDef> defEntry : instanceColumns.entrySet()) {
            Long insId = defEntry.getKey();
            ColumnDef def = defEntry.getValue();
            String name = def.getRealColumnName();
            if (filterClass) {
                WidgetInstance instance = getWidgetInstance(insId);
                if (widgetClass.getName().equals(instance.getWidgetName())) {
                    Object columnValue = tableBuilder.getColumnValue(containerId, record, name);
                    instanceColumnValues.put(insId, columnValue);
                }
            } else {
                Object columnValue = tableBuilder.getColumnValue(containerId, record, name);
                instanceColumnValues.put(insId, columnValue);
            }
        }
        return instanceColumnValues;
    }
}
